﻿#region Usings

using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Xml.Serialization;

using Gonte.Reflection; 

#endregion

namespace Gonte.Xml.Serialization
{
    /// <summary>
    /// Contains the information of how to serialize the type
    /// </summary>
    internal class TypeXmlMapping
    {
        private TypeXmlMapping(string typeName)
        {
            _typeName = typeName;
        }

        #region Methods

        /// <summary>
        /// Retrieves an xml type mapping for a type
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        internal static TypeXmlMapping GetMapping(Type type)
        {
            ExceptionThrower.ThrowWhenNull("type", type);

            type = TypeHelper.UnwrapType(type);

            if (_xmlTypeMappings.ContainsKey(type)) // To avoid cyclic references
            {
                return _xmlTypeMappings[type];
            }

            TypeXmlMapping xmlTypeMapping = new TypeXmlMapping(type.FullName);
            xmlTypeMapping._isAbstractType = type.IsAbstract || type.IsInterface;

            lock (((ICollection)_xmlTypeMappings).SyncRoot)
            {
                _xmlTypeMappings.Add(type, xmlTypeMapping);
            }

            xmlTypeMapping.PopulateMapping(type);

            return xmlTypeMapping;
        }

        /// <summary>
        /// Gets the name of the property that matches the element or attribute name
        /// </summary>
        /// <param name="name">The name of the element or attribute</param>
        /// <returns>The name of the property name</returns>
        internal string GetPropertyName(string name)
        {
            ExceptionThrower.ThrowWhenNot(
                _propertyMappings.ContainsKey(name),
                string.Format("There is no public property that matches the element: '{0}' in type: '{1}'",
                    name,
                    _typeName));

            return _propertyMappings[name].PropertyName;
        } 

        #endregion

        #region Properties

        public bool IsAbstractType
        {
            get { return _isAbstractType; }
            //set { _isAbstractType = value; }
        }

        internal bool HasAbstractMembers
        {
            get { return _hasAbstractMembers; }
            //set { _hasAbstractMembers = value; }
        }

        internal ICollection<PropertyXmlMapping> PropertyMappings
        {
            get { return _propertyMappings.Values; }
        } 

        #endregion

        #region Helpers

        private void PopulateMapping(Type type)
        {
            foreach (PropertyInfo propertyInfo in type.GetProperties())
            {
                Type propertyType = TypeHelper.UnwrapType(propertyInfo.PropertyType);
                PropertyXmlMapping propertyXmlMapping = new PropertyXmlMapping();

                if (Attribute.IsDefined(propertyInfo, typeof(XmlIgnoreAttribute))
                    || propertyType.IsSubclassOf(typeof(Delegate))) // Do not save
                {
                    propertyXmlMapping.Name = propertyInfo.Name;
                    propertyXmlMapping.PropertyName = propertyInfo.Name;
                    propertyXmlMapping.Save = false;
                }
                else
                {
                    propertyXmlMapping.Name = GetElementOrAttributeName(propertyInfo, out propertyXmlMapping.SaveAsAttribute);
                    propertyXmlMapping.PropertyName = propertyInfo.Name;
                    propertyXmlMapping.IsAbstract = propertyType.IsAbstract;

                    if (propertyXmlMapping.IsAbstract)
                    {
                        _hasAbstractMembers = true;
                    }

                    propertyXmlMapping.Save = true;
                }

                string name = propertyXmlMapping.Name;

                ExceptionThrower.ThrowWhen(
                    _propertyMappings.ContainsKey(name),
                    string.Format(
                        "Type: '{0}' has more than one property with the same xml element or attribute name: '{1}'",
                        _typeName,
                        name));

                _propertyMappings.Add(name, propertyXmlMapping);

                if (propertyXmlMapping.Save
                    && !PrimitiveType.IsPrimitive(propertyType)) // We need to traverse the complex proeprties to find whether they have abstract members
                {
                    TypeXmlMapping childMapping = GetMapping(propertyType);

                    if (false == _hasAbstractMembers)
                    {
                        _hasAbstractMembers = childMapping._hasAbstractMembers;
                    }
                }
            }
        }

        /// <summary>
        /// Returns the name of the element based on the name of its property
        /// </summary>
        /// <param name="propertyInfo"></param>
        /// <param name="saveAsAttribute"></param>
        /// <returns></returns>
        private string GetElementOrAttributeName(PropertyInfo propertyInfo, out bool saveAsAttribute)
        {
            string name = propertyInfo.Name;
            saveAsAttribute = false;

            XmlElementAttribute[] xmlElementAttributes = CustomAttribute.GetCustomAttributes<XmlElementAttribute>(propertyInfo, true);

            if (null != xmlElementAttributes)
            {
                foreach (XmlElementAttribute xmlElementAttribute in xmlElementAttributes)
                {
                    if (!string.IsNullOrEmpty(xmlElementAttribute.ElementName))
                    {
                        name = xmlElementAttribute.ElementName;
                    }

                    if (null != xmlElementAttribute.Type) // The type is specified for abstract fields or interface fields
                    {
                        _hasAbstractMembers = true;
                    }
                }
            }

            XmlAttributeAttribute[] xmlAttributeAttributes = CustomAttribute.GetCustomAttributes<XmlAttributeAttribute>(propertyInfo, true);

            if (null != xmlAttributeAttributes)
            {
                ExceptionThrower.ThrowWhen(null != xmlElementAttributes,
                    string.Format(
                        "Both XmlElement and XmlAttribute attributes are applied to property: '{0} in type: '{1}'",
                        propertyInfo.Name, _typeName));

                saveAsAttribute = true;

                foreach (XmlAttributeAttribute xmlAttributeAttribute in xmlAttributeAttributes)
                {
                    if (!string.IsNullOrEmpty(xmlAttributeAttribute.AttributeName))
                    {
                        name = xmlAttributeAttribute.AttributeName;
                        break;
                    }
                }
            }

            return name;
        }
 
        #endregion

        #region Inner classes

        /// <summary>
        /// How to persist the property to xml
        /// </summary>
        internal class PropertyXmlMapping
        {
            /// <summary>
            /// The name of the element or attribute
            /// </summary>
            internal string Name;

            /// <summary>
            /// The name of the property
            /// </summary>
            internal string PropertyName;

            /// <summary>
            /// Whether or not persist the object
            /// </summary>
            internal bool Save;

            /// <summary>
            /// Whether or not persist the object as an attribute. If false it will be persisted as an element
            /// </summary>
            internal bool SaveAsAttribute;

            /// <summary>
            /// Whether the type (or item type in case of a collection) is an abstract class or an interface
            /// </summary>
            internal bool IsAbstract;
        } 

        #endregion

        #region Fields

        private readonly string _typeName;
        private bool _isAbstractType;
        private bool _hasAbstractMembers;
        private readonly Dictionary<string, PropertyXmlMapping> _propertyMappings = new Dictionary<string, PropertyXmlMapping>();
        private readonly static Dictionary<Type, TypeXmlMapping> _xmlTypeMappings = new Dictionary<Type, TypeXmlMapping>(); 

        #endregion
    }
}
